/* 
 * This code is part of the notepad application, a sample application
 * for the SmartLink project, see
 * 
 * 		http://code.google.com/p/smartlink/.
 * 
 * It is a slightly modified version of the original notepad sample application 
 * distributed by Google as part of the Android SDK, see
 * 
 * 		http://code.google.com/android/samples/NotePad/index.html.
 * 
 * The modifications have the purpose of making the notepad application
 * acting as a "smartlink client" and "smartlink server" application.
 * What this means and the necessary requirements can be found in the documentation
 * of the SmartLink project. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.notepad;

import java.util.Map;

import org.smartlink.Client;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Menu;
import android.widget.EditText;

import com.google.provider.NotePad;

/**
 * A generic activity for editing a note in a database. This can be used either
 * to simply view a note (Intent.VIEW_ACTION), view and edit a note
 * (Intent.EDIT_ACTION), or create a new note (Intent.INSERT_ACTION).
 */
public class NoteEditor extends Activity {

	private static final String TAG = "NoteEditor";

	/** projection indices */
	private static final int NOTE_INDEX = 1;
	private static final int TITLE_INDEX = 2;
	private static final int MODIFIED_INDEX = 3;

	/**
	 * Standard projection for the interesting columns of a normal note.
	 */
	private static final String[] PROJECTION = new String[] { NotePad.Notes._ID, // 0
			NotePad.Notes.NOTE, // 1
			NotePad.Notes.TITLE, // 2
			NotePad.Notes.MODIFIED_DATE // 3
	};

	// This is our state data that is stored when freezing.
	private static final String ORIGINAL_CONTENT = "origContent";

	// Identifiers for our menu items.
	private static final int REVERT_ID = Menu.FIRST;
	private static final int DISCARD_ID = Menu.FIRST + 1;
	private static final int DELETE_ID = Menu.FIRST + 2;
	private static final int SMARTLINK_ID = Menu.FIRST + 3;

	// The different distinct states the activity can be run in.
	private static final int STATE_EDIT = 0;
	private static final int STATE_INSERT = 1;

	private int mState;
	private Uri mURI;
	private Cursor mCursor;
	private EditText mText;
	private String mOriginalContent;

	private boolean doNotRestoreText = false;

	private boolean uriValid;

	public static class MyEditText extends EditText {
		private Rect mRect;
		private Paint mPaint;

		// we need this constructor for ViewInflate
		@SuppressWarnings("unchecked")
		public MyEditText(Context context, AttributeSet attrs, Map params) {
			super(context, attrs, params);

			mRect = new Rect();
			mPaint = new Paint();
			mPaint.setStyle(Paint.Style.STROKE);
			mPaint.setColor(0xFF0000FF);
		}

		@Override
		protected void onDraw(Canvas canvas) {
			int count = getLineCount();
			Rect r = mRect;
			Paint paint = mPaint;
			for (int i = 0; i < count; i++) {
				int baseline = getLineBounds(i, r);
				canvas.drawLine(r.left, baseline + 1, r.right, baseline + 1, paint);
			}
			super.onDraw(canvas);
		}
	}

	@Override
	protected void onCreate(Bundle icicle) {
		super.onCreate(icicle);
		final Intent intent = getIntent();
		// Do some setup based on the action being performed.
		final String action = intent.getAction();
		if (action.equals(Intent.EDIT_ACTION) || action.equals(Intent.VIEW_ACTION)) {
			// Requested to edit: set that state, and the data being edited.
			mState = STATE_EDIT;
			mURI = intent.getData();
		} else if (action.equals(Intent.INSERT_ACTION)) {
			// Requested to insert: set that state, and create a new entry
			// in the container.
			mState = STATE_INSERT;
			mURI = getContentResolver().insert(intent.getData(), null);

			// If we were unable to create a new note, then just finish
			// this activity. A RESULT_CANCELED will be sent back to the
			// original activity if they requested a result.
			if (mURI == null) {
				Log.e("Notes", "Failed to insert new note into " + getIntent().getData());
				finish();
				return;
			}

			// The new entry was created, so assume all will end well and
			// set the result to be returned.
			setResult(RESULT_OK, mURI.toString());
		} else {
			// Whoops, unknown action! Bail.
			Log.e(TAG, "Unknown action, exiting");
			finish();
			return;
		}

		// Set the layout for this activity. You can find it
		// in res/layout/hello_activity.xml
		setContentView(R.layout.note_editor);

		// The text view for our note, identified by its ID in the XML file.
		mText = (EditText) findViewById(R.id.note);

		// Get the note!
		mCursor = managedQuery(mURI, PROJECTION, null, null);
		// we might end here with an invalid uri, so we have to check
		uriValid = (mCursor.count() == 1);
		if (!uriValid) {
			mCursor.close();
		}

		// If an instance of this activity had previously stopped, we can
		// get the original text it started with.
		if (icicle != null) {
			mOriginalContent = icicle.getString(ORIGINAL_CONTENT);
		}
	}

	@Override
	protected void onResume() {
		super.onResume();

		if (!uriValid) {
			setResult(RESULT_CANCELED);
			finish();
			return;
		}

		// If we didn't have any trouble retrieving the data, it is now
		// time to get at the stuff.
		if (mCursor != null) {
			// Make sure we are at the one and only row in the cursor.
			mCursor.first();

			// Modify our overall title depending on the mode we are running in.
			if (mState == STATE_EDIT) {
				setTitle(getText(R.string.title_edit));
			} else if (mState == STATE_INSERT) {
				setTitle(getText(R.string.title_create));
			}

			// This is a little tricky: we may be resumed after previously being
			// paused/stopped. We want to put the new text in the text view,
			// but leave the user where they were (retain the cursor position
			// etc). This version of setText does that for us.
			String note = mCursor.getString(NOTE_INDEX);

			if (!doNotRestoreText) {
				// do not restore the text after modifying the text by a
				// smartlink result
				mText.setTextKeepState(note);
			}

			// If we hadn't previously retrieved the original text, do so
			// now. This allows the user to revert their changes.
			if (mOriginalContent == null) {
				mOriginalContent = note;
			}

		} else {
			setTitle(getText(R.string.error_title));
			mText.setText(getText(R.string.error_message));
		}
	}

	@Override
	protected void onFreeze(Bundle outState) {
		// Save away the original text, so we still have it if the activity
		// needs to be killed while paused.
		outState.putString(ORIGINAL_CONTENT, mOriginalContent);
	}

	@Override
	protected void onPause() {
		super.onPause();

		if (!uriValid) {
			return;
		}
		// The user is going somewhere else, so make sure their current
		// changes are safely saved away in the provider. We don't need
		// to do this if only editing.
		if (mCursor != null) {
			String text = mText.getText().toString();
			int length = text.length();

			// If this activity is finished, and there is no text, then we
			// do something a little special: simply delete the note entry.
			// Note that we do this both for editing and inserting... it
			// would be reasonable to only do it when inserting.
			if (isFinishing() && uriValid && (length == 0)) {
				setResult(RESULT_CANCELED);
				deleteNote();

				// Get out updates into the provider.
			} else {
				// Bump the modification time to now.
				mCursor.updateLong(MODIFIED_INDEX, System.currentTimeMillis());

				// If we are creating a new note, then we want to also
				// create
				// an initial title for it.
				if (mState == STATE_INSERT) {
					String title = text.substring(0, Math.min(30, length));
					if (length > 30) {
						int lastSpace = title.lastIndexOf(' ');
						if (lastSpace > 0) {
							title = title.substring(0, lastSpace);
						}
					}
					mCursor.updateString(TITLE_INDEX, title);
				}
				// Write our text back into the provider.
				mCursor.updateString(NOTE_INDEX, text);

				// Commit all of our changes to persistent storage. Note the
				// use of managedCommitUpdates() instead of
				// mCursor.commitUpdates() -- this lets Activity take care of
				// requerying the new data if needed.
				managedCommitUpdates(mCursor);
			}
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		super.onCreateOptionsMenu(menu);

		// Build the menus that are shown when editing.
		if (mState == STATE_EDIT) {
			menu.add(0, REVERT_ID, R.string.menu_revert).setShortcut('0', 'r');
			menu.add(0, DELETE_ID, R.string.menu_delete).setShortcut('1', 'd');

			// Build the menus that are shown when inserting.
		} else {
			menu.add(0, DISCARD_ID, R.string.menu_discard).setShortcut('0', 'd');
		}

		// If we are working on a real honest-to-ghod note, then append to the
		// menu items for any other activities that can do stuff with it
		// as well. This does a query on the system for any activities that
		// implement the ALTERNATIVE_ACTION for our data, adding a menu item
		// for each one that is found.
		Intent intent = new Intent(null, getIntent().getData());
		intent.addCategory(Intent.ALTERNATIVE_CATEGORY);
		menu.addIntentOptions(Menu.ALTERNATIVE, 0, new ComponentName(this, NoteEditor.class), null, intent, 0, null);

		// insert smartlink entry point
		menu.add(0, SMARTLINK_ID, R.string.menu_smartlink);

		return true;
	}

	@Override
	public boolean onOptionsItemSelected(Menu.Item item) {
		// Handle all of the possible menu actions.
		switch (item.getId()) {
		case DELETE_ID:
			deleteNote();
			finish();
			break;
		case DISCARD_ID:
			cancelNote();
			break;
		case REVERT_ID:
			cancelNote();
			break;
		case SMARTLINK_ID:
			startSmartLink();
			break;
		}
		return super.onOptionsItemSelected(item);
	}

	/**
	 * Take care of cancelling work on a note. Deletes the note if we had
	 * created it, otherwise reverts to the original text.
	 */
	private final void cancelNote() {
		if (mCursor != null) {
			if (mState == STATE_EDIT) {
				mCursor.updateString(NOTE_INDEX, mOriginalContent);
				mCursor.commitUpdates();
				mCursor.deactivate();
				mCursor = null;
			} else if (mState == STATE_INSERT) {
				deleteNote();
			}
		}
		setResult(RESULT_CANCELED);
		finish();
	}

	/**
	 * Take care of deleting a note. Simply deletes the entry.
	 */
	private final void deleteNote() {
		if (mCursor != null) {
			mText.setText("");
			mCursor.deleteRow();
			mCursor.deactivate();
			mCursor = null;
		}
	}

	/** jumps into SmartLink based on the text selection */
	private void startSmartLink() {
		Uri contentUri = Uri.parse(Client.CONTENT_URI);
		Bundle bundle = new Bundle(1);
		CharSequence selection = getSelectedText();
		bundle.putString(Client.EXTRA_SEARCH_KEY, selection.toString());
		Intent intent = new Intent(Intent.PICK_ACTION, contentUri);
		intent.putExtras(bundle);
		startSubActivity(intent, 1);
	}

	private CharSequence getSelectedText() {
		int startIndex = Math.min(mText.getSelectionStart(), mText.getSelectionEnd());
		if (startIndex == -1) {
			Log.w(TAG, "returned -1");
			return "";
		}
		int endIndex = Math.max(mText.getSelectionStart(), mText.getSelectionEnd());
		Editable text = mText.getText();
		if (startIndex == endIndex) {
			// no selection, select word under cursor
			int textLength = text.length();
			// if (startIndex == textLength) {
			if (startIndex > 0) {
				startIndex--;
			}
			// }
			while (startIndex > 0 && text.charAt(startIndex) != ' ') {
				char c = text.charAt(startIndex - 1);
				if (isSeparatorChar(c)) {
					break;
				}
				startIndex--;
			}
			while (endIndex < textLength) {
				char c = text.charAt(endIndex);
				if (isSeparatorChar(c)) {
					break;
				}
				++endIndex;
			}
		}
		// trim leading space if necessary
		if (text.charAt(startIndex) == ' ') {
			startIndex++;
		}
		return text.subSequence(startIndex, endIndex);
	}

	/** returns true if the character is considered to separate entries. */
	private boolean isSeparatorChar(char c) {
		return Character.isSpaceChar(c) || c == '\n';
	}

	/** Listen for results of picking targets. */
	protected void onActivityResult(int requestCode, int resultCode, String data, Bundle extras) {
		// Log.w(TAG, "nonActivityResult(), request: " + requestCode);
		// Log.w(TAG, "nresultCode: " + resultCode);
		// Log.w(TAG, "ndata: " + data);
		// Log.w(TAG, "nbundle: " + extras);
		// TODO: switch depending on request or result code?
		if (data != null) {
			mText.getText().replace(mText.getSelectionEnd(), mText.getSelectionEnd(), data);
			doNotRestoreText = true;
		}
	}
}
